/* *******************************************************************
 * Copyright (c) 2007-2008 Contributors
 * All rights reserved.
 * This program and the accompanying materials are made available
 * under the terms of the Eclipse Public License v 2.0
 * which accompanies this distribution and is available at
 * https://www.eclipse.org/org/documents/epl-2.0/EPL-2.0.txt
 *
 * Contributors:
 *     Alexandre Vasseur
 * ******************************************************************/
package org.aspectj.weaver.patterns;

import org.aspectj.weaver.Member;

/**
 * A sample toString like visitor that helps understanding the AST tree structure organization
 *
 * @author <a href="mailto:alex AT gnilux DOT com">Alexandre Vasseur</a>
 */
public class DumpPointcutVisitor implements PatternNodeVisitor {

	private StringBuffer sb = new StringBuffer();

	public String get() {
		return sb.toString();
	}

	private void append(Object o) {
		sb.append(o.toString());
	}

	private void append(char c) {
		sb.append(c);
	}

	/**
	 * This method helps maintaining the API and raises warning when PatternNode subclasses do not implement the visitor pattern
	 *
	 * @param node
	 * @param data
	 * @return
	 */
	public Object visit(PatternNode node, Object data) {
		System.err.println("Should implement: " + node.getClass());
		return null;
	}

	public Object visit(AnyTypePattern node, Object data) {
		append('*');
		return null;
	}

	public Object visit(NoTypePattern node, Object data) {
		append(node.toString());// TODO no idea when this one is used
		return null;
	}

	public Object visit(EllipsisTypePattern node, Object data) {
		append(node.toString());
		return null;
	}

	public Object visit(AnyWithAnnotationTypePattern node, Object data) {
		if (node.getAnnotationPattern() != AnnotationTypePattern.ANY) {
			append('(');
		}
		node.annotationPattern.accept(this, data);
		append(" *");
		if (node.getAnnotationPattern() != AnnotationTypePattern.ANY) {
			append(')');
		}
		return null;
	}

	public Object visit(AnyAnnotationTypePattern node, Object data) {
		// @ANY : ignore
		append('*');
		return null;
	}

	public Object visit(EllipsisAnnotationTypePattern node, Object data) {
		append("..");
		return null;
	}

	public Object visit(AndAnnotationTypePattern node, Object data) {
		node.getLeft().accept(this, data);
		append(' ');
		node.getRight().accept(this, data);
		return null;
	}

	public Object visit(AndPointcut node, Object data) {
		append('(');
		node.getLeft().accept(this, data);
		append(" && ");
		node.getRight().accept(this, data);
		append(')');
		return null;
	}

	public Object visit(AndTypePattern node, Object data) {
		append('(');
		node.getLeft().accept(this, data);
		append(" && ");
		node.getRight().accept(this, data);
		append(')');
		return null;
	}

	public Object visit(AnnotationPatternList node, Object data) {
		AnnotationTypePattern[] annotations = node.getAnnotationPatterns();
		for (int i = 0; i < annotations.length; i++) {
			if (i > 0) {
				append(", ");// Note: list is ",", and is " " separated for annotations
			}
			annotations[i].accept(this, data);
		}
		return null;
	}

	public Object visit(AnnotationPointcut node, Object data) {
		append("@annotation(");
		node.getAnnotationTypePattern().accept(this, data);
		append(')');
		return null;
	}

	public Object visit(ArgsAnnotationPointcut node, Object data) {
		append("@args(");
		node.getArguments().accept(this, data);
		append(')');
		return null;
	}

	public Object visit(ArgsPointcut node, Object data) {
		append("args(");
		node.getArguments().accept(this, data);
		append(')');
		return null;
	}

	public Object visit(BindingAnnotationTypePattern node, Object data) {
		append(node);
		return null;
	}

	public Object visit(BindingTypePattern node, Object data) {
		append(node);
		return null;
	}

	public Object visit(CflowPointcut node, Object data) {
		append(node.isCflowBelow() ? "cflowbelow(" : "cflow(");
		node.getEntry().accept(this, data);
		append(')');
		return null;
	}

	public Object visit(ExactAnnotationTypePattern node, Object data) {
		// append('@'); // since @annotation(@someAnno) cannot be parsed anymore
		append(node.getAnnotationType().getName());
		return null;
	}

	public Object visit(ExactTypePattern node, Object data) {
		if (node.getAnnotationPattern() != AnnotationTypePattern.ANY) {
			append('(');
			node.getAnnotationPattern().accept(this, data);
			append(' ');
		}

		String typeString = node.getType().toString();
		if (node.isVarArgs()) {
			typeString = typeString.substring(0, typeString.lastIndexOf('['));// TODO AV - ugly
		}
		append(typeString);
		if (node.isIncludeSubtypes()) {
			append('+');
		}
		if (node.isVarArgs()) {
			append("...");
		}
		if (node.getAnnotationPattern() != AnnotationTypePattern.ANY) {
			append(')');
		}
		return null;
	}

	public Object visit(KindedPointcut node, Object data) {
		append(node.getKind().getSimpleName());
		append('(');
		node.getSignature().accept(this, data);
		append(')');
		return null;
	}

	public Object visit(ModifiersPattern node, Object data) {
		append(node.toString());// note: node takes care of forbidden mods
		return null;
	}

	public Object visit(NamePattern node, Object data) {
		append(node.toString());
		return null;
	}

	public Object visit(NotAnnotationTypePattern node, Object data) {
		append("!");
		node.getNegatedPattern().accept(this, data);
		return null;
	}

	public Object visit(NotPointcut node, Object data) {
		append("!(");
		node.getNegatedPointcut().accept(this, data);
		append(')');
		return null;
	}

	public Object visit(NotTypePattern node, Object data) {
		append("!(");
		node.getNegatedPattern().accept(this, data);
		append(')');
		return null;
	}

	public Object visit(OrAnnotationTypePattern node, Object data) {
		append('(');
		node.getLeft().accept(this, data);
		append(" || ");
		node.getRight().accept(this, data);
		append(')');
		return null;
	}

	public Object visit(OrPointcut node, Object data) {
		append('(');
		node.getLeft().accept(this, data);
		append(" || ");
		node.getRight().accept(this, data);
		append(')');
		return null;
	}

	public Object visit(OrTypePattern node, Object data) {
		append('(');
		node.getLeft().accept(this, data);
		append(" || ");
		node.getRight().accept(this, data);
		append(')');
		return null;
	}

	public Object visit(ReferencePointcut node, Object data) {
		append(node.toString());
		return null;
	}

	public Object visit(SignaturePattern node, Object data) {
		if (node.getAnnotationPattern() != AnnotationTypePattern.ANY) {
			node.getAnnotationPattern().accept(this, data);
			append(' ');
		}

		if (node.getModifiers() != ModifiersPattern.ANY) {
			node.getModifiers().accept(this, data);
			append(' ');
		}

		if (node.getKind() == Member.STATIC_INITIALIZATION) {
			node.getDeclaringType().accept(this, data);
		} else if (node.getKind() == Member.HANDLER) {
			append("handler(");
			node.getParameterTypes().get(0).accept(this, data);// Note: we know we have 1 child
			append(')');
		} else {
			if (!(node.getKind() == Member.CONSTRUCTOR)) {
				node.getReturnType().accept(this, data);
				append(' ');
			}
			if (node.getDeclaringType() != TypePattern.ANY) {
				node.getDeclaringType().accept(this, data);
				append('.');
			}
			if (node.getKind() == Member.CONSTRUCTOR) {
				append("new");
			} else {
				node.getName().accept(this, data);
			}
			if (node.getKind() == Member.METHOD || node.getKind() == Member.CONSTRUCTOR) {
				append('(');
				node.getParameterTypes().accept(this, data);
				append(')');
			}
			if (node.getThrowsPattern() != null) {
				append(' ');
				node.getThrowsPattern().accept(this, data);
			}
		}
		return null;
	}

	public Object visit(ThisOrTargetAnnotationPointcut node, Object data) {
		append(node.isThis() ? "@this(" : "@target(");
		node.getAnnotationTypePattern().accept(this, data);
		append(')');
		return null;
	}

	public Object visit(ThisOrTargetPointcut node, Object data) {
		append(node.isThis() ? "this(" : "target(");
		node.getType().accept(this, data);
		append(')');
		return null;
	}

	// Note: a visitor instance is not thread safe so should not be shared
	private boolean inThrowsForbidden = false;

	public Object visit(ThrowsPattern node, Object data) {
		if (node == ThrowsPattern.ANY) {
			return null;
		}

		append("throws ");
		node.getRequired().accept(this, data);
		if (node.getForbidden().size() > 0) {
			// a hack since throws !(A, B) cannot be parsed
			try {
				inThrowsForbidden = true;
				node.getForbidden().accept(this, data);
			} finally {
				inThrowsForbidden = false;
			}
		}
		return null;
	}

	public Object visit(TypePatternList node, Object data) {
		if (node.getTypePatterns().length == 0) {
			return null;
		}

		TypePattern[] typePatterns = node.getTypePatterns();
		for (int i = 0; i < typePatterns.length; i++) {
			TypePattern typePattern = typePatterns[i];
			if (i > 0) {
				append(", ");
			}
			if (inThrowsForbidden) {
				append('!');
			}
			typePattern.accept(this, data);
		}
		return null;
	}

	public Object visit(WildAnnotationTypePattern node, Object data) {
		append("@(");
		node.getTypePattern().accept(this, data);
		append(')');
		return null;
	}

	public Object visit(WildTypePattern node, Object data) {
		if (node.getAnnotationPattern() != AnnotationTypePattern.ANY) {
			append('(');
			node.getAnnotationPattern().accept(this, data);
			append(' ');
		}
		NamePattern[] namePatterns = node.getNamePatterns();
		for (int i = 0; i < namePatterns.length; i++) {
			if (namePatterns[i] == null) {
				append('.');// FIXME mh, error prone, can't we have a nullNamePattern ?
			} else {
				if (i > 0) {
					append('.');
				}
				namePatterns[i].accept(this, data);
			}
		}
		if (node.isIncludeSubtypes()) {
			append('+');
		}
		if (node.isVarArgs()) {
			append("...");
		}
		if (node.getAnnotationPattern() != AnnotationTypePattern.ANY) {
			append(')');
		}
		return null;
	}

	public Object visit(WithinAnnotationPointcut node, Object data) {
		append("@within(");
		node.getAnnotationTypePattern().accept(this, data);
		append(')');
		return null;
	}

	public Object visit(WithinCodeAnnotationPointcut node, Object data) {
		append("@withincode(");
		node.getAnnotationTypePattern().accept(this, data);
		append(')');
		return null;
	}

	public Object visit(WithinPointcut node, Object data) {
		append("within(");
		node.getTypePattern().accept(this, data);
		append(')');
		return null;
	}

	public Object visit(WithincodePointcut node, Object data) {
		append("withincode(");
		node.getSignature().accept(this, data);
		append(')');
		return null;
	}

	public Object visit(Pointcut.MatchesNothingPointcut node, Object data) {
		append("");// TODO shouldn't that be a "false" ?
		return null;
	}

	// -------------- perX

	public Object visit(PerCflow node, Object data) {
		append(node);
		return null;
	}

	public Object visit(PerFromSuper node, Object data) {
		append(node);
		return null;
	}

	public Object visit(PerObject node, Object data) {
		append(node);
		return null;
	}

	public Object visit(PerSingleton node, Object data) {
		append(node);
		return null;
	}

	public Object visit(PerTypeWithin node, Object data) {
		append(node);
		return null;
	}

	// ------------- declare X

	public Object visit(DeclareAnnotation node, Object data) {
		append(node);
		return null;
	}

	public Object visit(DeclareErrorOrWarning node, Object data) {
		append(node);
		return null;
	}

	public Object visit(DeclareParents node, Object data) {
		append(node);
		return null;
	}

	public Object visit(DeclarePrecedence node, Object data) {
		append(node);
		return null;
	}

	public Object visit(DeclareSoft node, Object data) {
		append(node);
		return null;
	}

	// ----------- misc

	public Object visit(ConcreteCflowPointcut node, Object data) {
		append(node);
		return null;
	}

	public Object visit(HandlerPointcut node, Object data) {
		append(node);
		return null;
	}

	public Object visit(IfPointcut node, Object data) {
		append(node);
		return null;
	}

	public Object visit(TypeVariablePattern node, Object data) {
		append(node);
		return null;
	}

	public Object visit(TypeVariablePatternList node, Object data) {
		append(node);
		return null;
	}

	public Object visit(HasMemberTypePattern node, Object data) {
		append(node);
		return null;
	}

	public Object visit(TypeCategoryTypePattern node, Object data) {
		append(node);
		return null;
	}

	public static void check(String s) {
		check(Pointcut.fromString(s), false);
	}

	public static void check(PatternNode pc, boolean isTypePattern) {
		DumpPointcutVisitor v1 = new DumpPointcutVisitor();
		pc.accept(v1, null);

		DumpPointcutVisitor v2 = new DumpPointcutVisitor();
		final PatternNode pc2;
		if (isTypePattern) {
			pc2 = new PatternParser(v1.get()).parseTypePattern();
		} else {
			pc2 = Pointcut.fromString(v1.get());
		}
		pc2.accept(v2, null);

		// at second parsing, the String form stay stable when parsed and parsed again
		if (!v1.get().equals(v2.get())) {
			throw new ParserException("Unstable back parsing for '" + pc + "', got '" + v1.get() + "' and '" + v2.get() + "'", null);
		}
	}

	public static void main(String args[]) throws Throwable {
		String[] s = new String[] {
		// "@args(Foo, Goo, *, .., Moo)",
		// "execution(* *())",
		// "call(* *(int, Integer...))",
		// "staticinitialization(@(Foo) @(Boo) @(Goo) Moo)",
		"(if(true) && set(int BaseApp.i))"

		};
		for (String value : s) {
			check(value);
		}
	}

}
